package lsp

import (
	"net"
	"regexp"
	"strings"
	"unicode"
)

var stringSchemaMessage = map[string]string{
	"alphanum":  "{{#label}} must only contain alpha-numeric characters",
	"domain":    "{{#label}} must contain a valid domain name",
	"email":     "{{#label}} must be a valid email",
	"required":  "{{#label}} is not allowed to be empty",
	"ip":        "{{#label}} must be a valid ip address",
	"length":    "{{#label}} length must be {{#limit}} characters long",
	"max":       "{{#label}} length must be less than or equal to {{#limit}} characters long",
	"min":       "{{#label}} length must be at least {{#limit}} characters long",
	"lowercase": "{{#label}} must only contain lowercase characters",
	"uppercase": "{{#label}} must only contain uppercase characters",
	"trim":      "{{#label}} must not have leading or trailing whitespace",
	"equal":     "{{#label}} must be {{#target}}",
	"not":       "{{#label}} cannot be {{#target}}",
	"valid":     "{{#label}} must be one of {{#target}}",
	"invalid":   "{{#label}} cannot be one of {{#target}}",
}

func NewString() *stringSchema {
	return &stringSchema{
		rules:     map[string]func(value string) error{},
		whitelist: make([]string, 0),
		blacklist: make([]string, 0),
		anySchema: anySchema{
			label:   "value",
			message: stringSchemaMessage,
		},
	}
}

type stringSchema struct {
	anySchema
	whitelist []string
	blacklist []string
	rules     map[string]func(value string) error
}

func (schema *stringSchema) addRule(method string, hook func(value string) error) *stringSchema {
	schema.rules[method] = hook
	return schema
}

// Validate a value using the schema.
func (schema *stringSchema) Validate(value string) error {
	for _, hook := range schema.rules {
		err := hook(value)
		if nil != err {
			return err
		}
	}
	return nil
}

// Custom adds a custom validation function.
func (schema *stringSchema) Custom(fn func(value string) error) *stringSchema {
	return schema.addRule("custom", fn)
}

// Label overrides the key name in error messages.
func (schema *stringSchema) Label(name string) *stringSchema {
	schema.label = name
	return schema
}

// Required requires the string value is not allowed to be empty.
func (schema *stringSchema) Required() *stringSchema {
	return schema.addRule("required", func(value string) error {
		if "" == value {
			return schema.error("required", nil)
		}
		return nil
	})
}

// Alphanum requires the string value to only contain a-z, A-Z, and 0-9.
func (schema *stringSchema) Alphanum() *stringSchema {
	return schema.addRule("alphanum", func(value string) error {
		if regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(value) {
			return nil
		}
		return schema.error("alphanum", nil)
	})
}

// Domain requires the string value to be a valid domain.
func (schema *stringSchema) Domain() *stringSchema {
	return schema.addRule("domain", func(value string) error {
		// TODO
		return nil
	})
}

// Email requires the string value to be a valid email address.
func (schema *stringSchema) Email() *stringSchema {
	return schema.addRule("email", func(value string) error {
		if regexp.MustCompile("\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*").MatchString(value) {
			return nil
		}
		return schema.error("email", nil)
	})
}

// IP requires the string value to be a valid ip address.
func (schema *stringSchema) IP() *stringSchema {
	return schema.addRule("ip", func(value string) error {
		if nil == net.ParseIP(value) {
			return schema.error("ip", nil)
		}
		return nil
	})
}

// Length specifies the exact string length required.
// limit - the required string length.
func (schema *stringSchema) Length(limit int) *stringSchema {
	return schema.addRule("length", func(value string) error {
		if len(value) == limit {
			return nil
		}
		return schema.error("length", map[string]interface{}{"limit": limit})
	})
}

// Max specifies the maximum number of string characters.
// limit - the maximum number of string characters allowed.
func (schema *stringSchema) Max(limit int) *stringSchema {
	return schema.addRule("max", func(value string) error {
		if len(value) <= limit {
			return nil
		}
		return schema.error("max", map[string]interface{}{"limit": limit})
	})
}

// Min specifies the minimum number string characters.
// limit - the minimum number of string characters required.
func (schema *stringSchema) Min(limit int) *stringSchema {
	return schema.addRule("min", func(value string) error {
		if len(value) >= limit {
			return nil
		}
		return schema.error("min", map[string]interface{}{"limit": limit})
	})
}

// Lowercase requires the string value to be all lowercase.
func (schema *stringSchema) Lowercase() *stringSchema {
	return schema.addRule("lowercase", func(value string) error {
		for _, v := range value {
			if unicode.IsUpper(v) {
				return schema.error("lowercase", nil)
			}
		}
		return nil
	})
}

// Uppercase requires the string value to be all uppercase.
func (schema *stringSchema) Uppercase() *stringSchema {
	return schema.addRule("uppercase", func(value string) error {
		for _, v := range value {
			if unicode.IsLower(v) {
				return schema.error("uppercase", nil)
			}
		}
		return nil
	})
}

// Trim requires the string value to contain no whitespace before or after.
func (schema *stringSchema) Trim() *stringSchema {
	return schema.addRule("trim", func(value string) error {
		if strings.HasPrefix(value, " ") || strings.HasSuffix(value, " ") {
			return schema.error("trim", nil)
		}
		return nil
	})
}

// Equal requires the string value for target.
// target - allowed target values.
func (schema *stringSchema) Equal(target string) *stringSchema {
	return schema.addRule("equal", func(value string) error {
		if value == target {
			return nil
		}
		return schema.error("equal", map[string]interface{}{"target": target})
	})
}

// Not required value cannot be the target value.
// target - disallow target values.
func (schema *stringSchema) Not(target string) *stringSchema {
	return schema.addRule("not", func(value string) error {
		if value == target {
			return schema.error("not", map[string]interface{}{"target": target})
		}
		return nil
	})
}

// Allow whitelists a value
func (schema *stringSchema) Allow(values ...string) *stringSchema {
	schema.whitelist = append(schema.whitelist, values...)
	return schema
}

// Valid adds the provided values into the allowed whitelist and marks them as the only valid values allowed.
func (schema *stringSchema) Valid(values ...string) *stringSchema {
	schema.Allow(values...)
	return schema.addRule("valid", func(value string) error {
		for _, val := range schema.whitelist {
			if val == value {
				return nil
			}
		}
		return schema.error("valid", map[string]interface{}{"target": schema.whitelist})
	})
}

// Disallow blacklists a value
func (schema *stringSchema) Disallow(values ...string) *stringSchema {
	schema.blacklist = append(schema.blacklist, values...)
	return schema
}

// Invalid adds the provided values into the allowed blacklist and marks them as the only invalid values disallow.
func (schema *stringSchema) Invalid(values ...string) *stringSchema {
	schema.Disallow(values...)
	return schema.addRule("invalid", func(value string) error {
		for _, val := range schema.blacklist {
			if val == value {
				return schema.error("invalid", map[string]interface{}{"target": schema.blacklist})
			}
		}
		return nil
	})
}
